Go 语言的 sync 包是其并发编程模型的基石,提供了实现同步和并发控制的关键原语。在过去的两年里(大约从 Go 1.19 到 1.25),sync 包及其子包 sync/atomic 经历了一系列重要的演进。这些变化不仅包括新功能的增加,还涉及性能优化、内部实现的重构以及开发者体验的显著提升。
本文基于 Go 语言官方仓库的 Git 提交历史,对 sync 包近两年的主要变化进行总结。
1. 新增 API 与功能增强
为了简化常见的并发模式,sync 包引入了一些备受期待的新 API。
sync.WaitGroup.Go
Go 1.25 引入了 WaitGroup.Go 方法,极大地简化了在 WaitGroup 中启动 goroutine 的代码。
旧模式:
|
|
新模式:
|
|
这个辅助方法不仅减少了样板代码,还通过内置的 defer wg.Done() 调用避免了忘记调用 Done() 的常见错误。
代码示例:
|
|
sync.Map.Clear 和 sync.Map.Swap
sync.Map 也获得了一些实用的新方法:
Clear(): 用于一次性删除Map中的所有键值对,提供了一种高效清空Map的标准方式 (Go 1.23.0)。Swap(): 原子性地交换一个键的新旧值 (Go 1.20)。CompareAndSwap()/CompareAndDelete(): 提供了更精细的原子操作,允许用户基于旧值进行条件交换或删除 (Go 1.20)。
代码示例:
|
|
sync.Once 系列函数的演进
Go 1.21.0 引入的 OnceFunc, OnceValue, OnceValues 在后续版本中得到了优化,例如减少了堆内存分配,使其在需要延迟初始化或缓存计算结果的场景下更高效。func
OnceFunc(f func()) func():将一个无参数无返回值的函数包装成只执行一次的函数。func OnceValue[T any](f func() T) func() T: 将一个无参数但有单个返回值的函数包装成只执行一次的函数,后续调用返回缓存的结果。func OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2): 将一个无参数但有两个返回值的函数包装成只执行一次的函数,后续调用返回缓存的结果。
代码示例 (OnceValue)
|
|
2. 性能优化与内部实现重构
sync 包的性能至关重要。近期的更新在多个方面对其进行了优化。
sync.Map 的新实现
sync.Map 的内部实现经历了一次重大变更,引入了基于哈希分片三角树(HashTrieMap)的新设计 (Go 1.24)。但这类探索表明了社区在持续寻求提升 sync.Map 在不同并发场景下性能的努力。
sync.Mutex 内部重构
Mutex 在Go1.24.0增加了一个基于HashTrieMap的实现。在Go1.26.0中应该会移除旧的sync.Mutex实现, sync.Map将默认采用HashTrieMap的实现。
https://github.com/golang/go/issues/70683
sync.Once 的原子操作优化
Once.done 字段的实现从 atomic.Uint32 切换到atomic.Bool (Go 1.25)。这不仅提升了代码的可读性和类型安全性,也代表了用现代原子类型替代旧有模式的趋势。
3. 代码正确性与开发者体验
为帮助开发者编写更健壮的并发代码,sync 包在文档和静态分析方面做了大量改进。
noCopy 哨兵的引入
Mutex, RWMutex, WaitGroup, Cond, 和 Map 等核心类型都加入了 noCopy 字段。这是一个特殊的非导出字段,可以被 go vet 工具识别。当开发者无意中复制了这些包含内部状态的同步原语时,go vet 会发出警告,从而在编译前发现潜在的并发 bug。
代码示例 (错误用法):
|
|
文档的持续改进
大量的提交都致力于改进和澄清文档,包括:
- 明确指出
RWMutex的读锁和写锁不能相互升级或降级。 - 详细描述了
Map的Range、Delete等方法在并发访问下的行为和内存模型保证。 - 为
Cond.Wait的行为提供了更清晰的说明。 - 在包文档中直接链接到 Go 内存模型,强调其重要性。
4. sync/atomic 的现代化
作为 sync 的底层支撑,sync/atomic 包在 Go 1.19 中引入了基于泛型的类型安全原子类型(如 atomic.Int64, atomic.Pointer[T], atomic.Bool 等)。近两年的变化主要体现在:
- 鼓励使用新类型:在
sync包内部,旧的函数式原子操作(如atomic.LoadUint32)正逐渐被新的类型化方法(如myAtomicBool.Load())所取代。 - API 完善:增加了
And/Or等新的原子位操作函数,并对文档进行了补充,以指导用户从旧 API 过渡到新 API。
总结
过去两年,Go 的 sync 包在保持 API 稳定的同时,向着更易用、更安全、更高性能的方向稳步发展。通过引入 WaitGroup.Go 等便捷的辅助函数,开发者可以编写更简洁的并发代码。noCopy 哨兵和持续完善的文档则提高了代码的健壮性。底层的性能优化和实现重构,确保了 Go 的并发原语能够适应不断增长的性能需求。这些变化共同巩固了 Go 作为一门现代并发语言的地位。
